﻿using System.Windows.Controls;
using System.Windows;
using System.Windows.Data;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.ComponentModel;
using Inspire.Interfaces;
using System;

namespace Inspire.Main.UIElements
{
    class ErrorProvider : Decorator
    {
        private FrameworkElement _firstInvalidElement = null;
        private List<string> _errorList = new List<string>();
        private List<ElementBindingDp> _bindings;

        /// <summary>
        /// First element found to be invalid. May be used for giving focus to the first invalid form field.
        /// </summary>
        public FrameworkElement FirstInvalidElement
        {
            get
            {
                return this._firstInvalidElement;
            }
        }

        /// <summary>
        /// A List structure of all errors found in the DataContext.
        /// </summary>
        public List<string> ErrorList
        {
            get
            {
                return this._errorList;
            }
        }


        /// <summary>
        /// Delegate for notifying methods of when a binding is found by the FindBindingsRecursively() method.
        /// </summary>
        /// <param name="element"></param>
        /// <param name="binding"></param>
        /// <param name="dp"></param>
        private delegate void FoundBindingCallbackDelegate(FrameworkElement element, Binding binding, DependencyProperty dp);

        /// <summary>
        /// Constructor.
        /// </summary>
        public ErrorProvider()
        {
            this.Loaded += new RoutedEventHandler(ErrorProvider_Loaded);
            this.DataContextChanged += new DependencyPropertyChangedEventHandler(this.ErrorProvider_DataContextChanged);
        }

        /// <summary>
        /// Validates the children of this ErrorProvider when it is first loaded.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void ErrorProvider_Loaded(object sender, RoutedEventArgs args)
        {
            // Get all known FrameworkElement DependencyProperty _bindings
            this._bindings = this.FindBindings();

            Validate();
        }

        /// <summary>
        /// If the values of any DependencyProperty Properties or Fields changes, add a PropertyChanged listener to the NewValue object.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        public void ErrorProvider_DataContextChanged(object sender, DependencyPropertyChangedEventArgs args)
        {
            if (args.NewValue != null && (args.NewValue) is INotifyPropertyChanged)
            {
                ((INotifyPropertyChanged)args.NewValue).PropertyChanged += new PropertyChangedEventHandler(ErrorProvider_PropertyChanged);
            }
        }

        /// <summary>
        /// Handle value changes of the Bound DependencyProperty Properties and Fields.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public void ErrorProvider_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "IsValid")
            {
                return;
            }
           Validate();
        }

        /// <summary>
        /// Cycles through all FrameworkElement items within this Decorator and checks if their DataContext is set to an IDataErrorInfo object;
        /// if it is, it checks to see if any of the element's _bindings are bound to a DependencyProperty that is in message. Generally, such an object
        /// will be an Entity (a record from the database encapsulated in an object).
        /// </summary>
        /// <returns></returns>
        public bool Validate()
        {
            // Initialize Validated to true
            bool isValid = true;

            // Clear the message list
            this._errorList.Clear();

            // Reset the first invalid element
            this._firstInvalidElement = null;

            // The ErrorProvider's DataContext should be the object that all its children get their data from
            if (this.DataContext is IDataErrorInfo)
            {
                // Cycle through the elements and see if any of their IDataErrorInfo have invalid Properties
                foreach (ElementBindingDp nextBinding in _bindings)
                {
                    string errorMessage = ((IDataErrorInfo)nextBinding.element.DataContext)[nextBinding.binding.Path.Path];

                    // If there's an message with the bound Property, mark the FrameworkElement as invalid
                    if (errorMessage != null && errorMessage.Length > 0)
                    {
                        // Add the Error Message to the Error List
                        this._errorList.Add(errorMessage);

                        // Create the BindingExpression and add a ValidationError to the Binding
                        BindingExpression bindingExpression = nextBinding.element.GetBindingExpression(nextBinding.dependencyProperty);
                        Validation.MarkInvalid(bindingExpression, new ValidationError(new ExceptionValidationRule(), bindingExpression, errorMessage, null));

                        // If this is the first element, set the _firstInvalidElement field
                        if (this._firstInvalidElement == null)
                        {
                            this._firstInvalidElement = nextBinding.element;
                        }

                        isValid = false;
                    }
                    else
                    {
                        // If the field still has errors, clear them
                        if (Validation.GetHasError(nextBinding.element))
                        {
                            Validation.ClearInvalid(nextBinding.element.GetBindingExpression(nextBinding.dependencyProperty));
                        }
                    }
                }
            }

            return isValid;
        }

        /// <summary>
        /// If the DataContext implements IValidation, Validate all DependencyProperty properties (even if they haven't changed).
        /// </summary>
        /// <returns>(bool) True if validation is successful; false, otherwise.</returns>
        public bool ValidateFinal()
        {
            if (this.DataContext is IValidation)
            {
                ((IValidation)this.DataContext).Validate();
                return this.Validate();
            }
            return false;
        }

        /// <summary>
        /// Recurse through the children of this ErrorProvider and find all DependencyProperty Properties and Fields
        /// that are bound.
        /// </summary>
        /// <returns></returns>
        private List<ElementBindingDp> FindBindings()
        {
            List<ElementBindingDp> bindings = new List<ElementBindingDp>();
            
            FindBindingsRecursively(
                this,
                delegate(FrameworkElement element, Binding binding, DependencyProperty dp)
                {
                    bindings.Add(new ElementBindingDp(element, binding, dp));
                }
            );

            return bindings;
        }

        /// <summary>
        /// Recurse through the children of a DependencyObject and find all DependencyProperty Properties and Fields
        /// that are bound.
        /// </summary>
        /// <param name="element"></param>
        /// <param name="callbackDelegate"></param>
        private void FindBindingsRecursively(DependencyObject element, FoundBindingCallbackDelegate callbackDelegate)
        {
            MemberInfo[] members = element.GetType().GetMembers(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);

            foreach (MemberInfo member in members)
            {
                DependencyProperty dp = null;

                // If it's a field, check if it is a dependency property
                if (member.MemberType == MemberTypes.Field)
                {
                    FieldInfo field = (FieldInfo)member;
                    if (typeof(DependencyProperty).IsAssignableFrom(field.FieldType))
                    {
                        dp = (DependencyProperty)field.GetValue(element);
                    }
                }

                // If it's a Member, check if it is a dependency property
                else if (member.MemberType == MemberTypes.Property)
                {
                    PropertyInfo property = (PropertyInfo)member;
                    if (typeof(DependencyProperty).IsAssignableFrom(property.PropertyType))
                    {
                        dp = (DependencyProperty)property.GetValue(element, null);
                    }
                }

                // If it's a dependency property, check if it has a binding
                if (dp != null)
                {
                    Binding binding = BindingOperations.GetBinding(element, dp);
                    if (binding != null)
                    {
                        if (element is FrameworkElement)
                        {
                            callbackDelegate((FrameworkElement)element, binding, dp);
                        }
                    }
                }
            }
            
            // If it's a FrameworkElement, check it's children
            if (element is FrameworkElement || element is FrameworkContentElement)
            {
                foreach (object childElement in LogicalTreeHelper.GetChildren(element))
                {
                    if (childElement is DependencyObject)
                    {
                        FindBindingsRecursively((DependencyObject)childElement, callbackDelegate);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Stores the Element, the binding, and the dependency property the binding belongs too for later use
    /// </summary>
    class ElementBindingDp
    {
        public FrameworkElement element;
        public Binding binding;
        public DependencyProperty dependencyProperty;

        public ElementBindingDp(FrameworkElement element, Binding binding, DependencyProperty dependencyProperty)
        {
            this.element = element;
            this.binding = binding;
            this.dependencyProperty = dependencyProperty;
        }
    }
}
